fix(desktop): declare Bluetooth usage strings so the sidecar stops crashing on Finder launch#88
Conversation
…ashing on Finder launch
The notarized desktop bundle hung forever on the splash ("正在启动本地服务")
when launched from Finder, while `make desktop-dev` worked fine.
Root cause: the sidecar links CoreBluetooth (tinygo.org/x/bluetooth, the BLE
status channel). When any CoreBluetooth API is touched, macOS requires
NSBluetoothAlwaysUsageDescription in the bundle Info.plist — without it the
process is killed with SIGABRT (a TCC abort) before the web server ever binds,
with no output on its stderr pipe. So /api/health never responds and the
window never navigates off the splash.
It only reproduces on a Finder/LaunchServices launch: from a terminal the
responsible app already holds the Bluetooth grant, which is why dev (and running
the binary by hand) worked. `jcode --version` also survives because it exits
before CoreBluetooth's async state callback fires.
Changes:
- Add desktop/src-tauri/Info.plist with NSBluetoothAlwaysUsageDescription and
NSBluetoothPeripheralUsageDescription. Tauri auto-merges src-tauri/Info.plist
into the bundle Info.plist (verified: `make desktop-build` then
`plutil -p .../Contents/Info.plist | grep Bluetooth`). macOS now prompts /
denies gracefully instead of aborting.
- Harden sidecar.rs: tee the sidecar's stdout/stderr to
app_log_dir()/jcode-sidecar.log plus a recent-lines ring buffer, and surface
a failure dialog (with the tail + log path) when the sidecar exits before it
becomes healthy, instead of leaving the splash spinning forever.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
📝 WalkthroughWalkthroughAdds macOS Bluetooth usage description entries to ChangesSidecar startup failure surface and macOS Bluetooth permission
Sequence Diagram(s)sequenceDiagram
participant Pump as stdout/stderr pump
participant Health as health poll thread
participant Surface as surface_startup_failure
participant Dialog as blocking dialog thread
rect rgba(100, 149, 237, 0.5)
note over Pump: Normal output
Pump->>Pump: write line → log file + ring buffer
end
rect rgba(220, 80, 80, 0.5)
note over Pump,Health: Failure paths
Pump->>Surface: sidecar exits before ready
Health->>Health: poll health_ok() × HEALTH_POLL_ATTEMPTS
Health->>Surface: attempts exhausted
end
Surface->>Dialog: spawn thread → blocking dialog(log_path, recent tail)
Dialog-->>Surface: closed
Surface->>Surface: process::exit(1)
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~20 minutes Poem
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@desktop/src-tauri/src/sidecar.rs`:
- Around line 131-139: The CommandEvent::Terminated handler calls
surface_startup_failure when the sidecar exits before the app is ready, but it
never sets pump_ready to true. This creates a race condition where the poll
thread (at line 174) can also detect that ready is false and call
surface_startup_failure a second time, resulting in duplicate error dialogs.
After calling surface_startup_failure in the CommandEvent::Terminated branch,
add pump_ready.store(true, Ordering::SeqCst) to prevent the poll thread from
also triggering the failure handler. Additionally, the log message at line 133
unconditionally reports "exited before ready" even for normal shutdowns after
the app became healthy, so consider making this log conditional to only appear
when pump_ready is actually false.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: b8a50301-2372-47ab-88c4-367cf24d27c8
📒 Files selected for processing (2)
desktop/src-tauri/Info.plistdesktop/src-tauri/src/sidecar.rs
Problem
The notarized desktop bundle hangs forever on the splash (正在启动本地服务) when launched from Finder, even though
make desktop-devworks fine. The local server never comes up.Root cause
The sidecar binary links CoreBluetooth (
tinygo.org/x/bluetooth, the optional BLE status channel). The moment any CoreBluetooth API is touched, macOS requiresNSBluetoothAlwaysUsageDescriptionin the bundleInfo.plist. Without it, the process is killed with SIGABRT (a TCC privacy abort) before the web server ever binds — and the abort goes totccd's os_log, not the process stderr, so there's zero output./api/healththerefore never responds and the window never navigates off the splash.Why it was so confusing:
make desktop-devand running the binary by hand worked. Only a Finder/LaunchServices launch (responsible =jcode-desktop, no grant) aborts.jcode --versionsurvives (it exits before CoreBluetooth's async state callback fires); onlyjcode webdies. This misleads toward Go-version and hardened-runtime theories — both red herrings (ad-hoc-hardened + Go 1.25.8 builds run fine from a terminal).Changes
desktop/src-tauri/Info.plist(new) — declaresNSBluetoothAlwaysUsageDescription+NSBluetoothPeripheralUsageDescription. Tauri auto-mergessrc-tauri/Info.plistinto the generated bundleInfo.plist. With the string present, macOS shows a permission prompt / denies gracefully instead of aborting.desktop/src-tauri/src/sidecar.rs— defense-in-depth so a dying sidecar is never a silent infinite splash: tee the sidecar's stdout/stderr toapp_log_dir()/jcode-sidecar.log+ a recent-lines ring buffer, and on exit-before-ready show a failure dialog (with the output tail + log path) and quit.Verification
make desktop-build→plutil -p .../jcode.app/Contents/Info.plist | grep Bluetoothshows both keys present (Tauri merge confirmed).cargo checkon the desktop crate passes with thesidecar.rschanges.Reviewer notes
make desktop-devdoes not catch this class of bug (it spawns the sidecar from a terminal context). Finder/openlaunch is required to reproduce native launch-context issues.cfg.Channel.BLEEnabled; this PR just makes touching CoreBluetooth non-fatal at the OS level.Summary by CodeRabbit
Release Notes
Bug Fixes
Chores